{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第五章 系统评估与优化\n",
    "\n",
    "## 5.1 如何评估 LLM 应用"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1.1 验证评估的一般思路"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，我们已经构建了一个简单的、一般化的大模型应用。回顾整个开发流程，我们可以发现，以调用、发挥大模型为核心的大模型开发相较传统的 AI 开发更注重验证迭代。由于你可以快速构建出基于 LLM 的应用程序，在几分钟内定义一个 Prompt，并在几小时内得到反馈结果，那么停下来收集一千个测试样本就会显得极为繁琐。因为现在，你可以在没有任何训练样本的情况下得到结果。\n",
    "\n",
    "因此，在使用LLM构建应用程序时，你可能会经历以下流程：首先，你会在一到三个样本的小样本中调整 Prompt ，尝试使其在这些样本上起效。随后，当你对系统进行进一步测试时，可能会遇到一些棘手的例子，这些例子无法通过 Prompt 或者算法解决。这就是使用 LLM 构建应用程序的开发者所面临的挑战。在这种情况下，你可以将这些额外的几个例子添加到你正在测试的集合中，有机地添加其他难以处理的例子。最终，你会将足够多的这些例子添加到你逐步扩大的开发集中，以至于手动运行每一个例子以测试 Prompt 变得有些不便。然后，你开始开发一些用于衡量这些小样本集性能的指标，例如平均准确度。这个过程的有趣之处在于，如果你觉得你的系统已经足够好了，你可以随时停止，不再进行改进。实际上，很多已经部署的应用程序就在第一步或第二步就停下来了，而且它们运行得非常好。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![](../../assets/C5-1-eval.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在本章中，我们将逐个介绍大模型应用验证评估的一般方法，并设计本项目验证迭代的过程，从而实现应用功能的优化。但是注意，由于系统评估与优化是一个与业务密切相关的话题，本章我们以理论介绍为主，欢迎读者积极进行自我实践和探索。\n",
    "\n",
    "我们将首先介绍大模型开发评估的几种方法。对于有简单标准答案的任务来说，评估很容易得到实现；但大模型开发一般是需要实现复杂的生成任务，如何在没有简单答案甚至没有标准答案的情况下实现评估，能够准确地反映应用的效果，我们将简要介绍几种方法。\n",
    "\n",
    "而随着我们不断寻找到 Bad Case 并做出针对性优化，我们可以将这些 Bad Case 逐步加入到验证集，从而形成一个有一定样例数的验证集。针对这种验证集，一个一个进行评估就是不切实际的了。我们需要一种自动评估方法，实现对该验证集上性能的整体评估。\n",
    "\n",
    "掌握了一般思路，我们会具体到基于 RAG 范式的大模型应用中来探究如何评估并优化应用性能。由于基于 RAG 范式开发的大模型应用一般包括两个核心部分：检索和生成，所以，我们的评估优化也会分别聚焦到这两个部分，分别以优化系统检索精度和在确定给定材料下的生成质量。\n",
    "\n",
    "在每一个部分，我们都会首先介绍如何找出 Bad Case 的一些思路提示，以及针对 Bad Case 针对性做出检索优化或 Prompt 优化的一般思路。注意，在这一过程中，你应该时刻谨记我们在之前章节中所讲述的一系列大模型开发原则与技巧，并时刻保证优化后的系统不会在原先表现良好的样例上出现失误。\n",
    "\n",
    "验证迭代是构建以 LLM 为中心的应用程序所必不能少的重要步骤，通过不断寻找 Bad Case，针对性调整 Prompt 或优化检索性能，来推动应用达到我们目标中的性能与精度。接下来，我们将简要介绍大模型开发评估的几种方法，并概括性介绍从少数 Bad Case 针对性优化到整体自动化评估的一般思路。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1.2 大模型评估方法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在具体的大模型应用开发中，我们可以找到 Bad Cases，并不断针对性优化 Prompt 或检索架构来解决 Bad Cases，从而优化系统的表现。我们会将找到的每一个 Bad Case 都加入到我们的验证集中，每一次优化之后，我们会重新对验证集中所有验证案例进行验证，从而保证优化后的 系统不会在原有 Good Case 上失去能力或表现降级。当验证集体量较小时，我们可以采用人工评估的方法，即对验证集中的每一个验证案例，人工评估系统输出的优劣；但是，当验证集随着系统的优化而不断扩张，其体量会不断增大，以至于人工评估的时间和人力成本扩大到我们无法接受的程度。因此，我们需要采用自动评估的方法，自动评估系统对每一个验证案例的输出质量，从而评估系统的整体性能。\n",
    "\n",
    "我们将首先介绍人工评估的一般思路以供参考，接着深入介绍大模型自动评估的一般方法，并在本系统上进行实际验证，全面评估本系统表现，为系统的进一步优化迭代做准备。同样，在正式开始之前，我们先加载我们的向量数据库与检索链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/yd/c4q_f88j1l70g7_jcb6pdnb80000gn/T/ipykernel_25665/3013114604.py:23: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-chroma package and should be used instead. To use it run `pip install -U :class:`~langchain-chroma` and import as `from :class:`~langchain_chroma import Chroma``.\n",
      "  vectordb = Chroma(\n"
     ]
    }
   ],
   "source": [
    "import sys\n",
    "sys.path.append(\"../C3 搭建知识库\") # 将父目录放入系统路径中\n",
    "\n",
    "# 使用智谱 Embedding API，注意，需要将上一章实现的封装代码下载到本地\n",
    "from zhipuai_embedding import ZhipuAIEmbeddings\n",
    "\n",
    "from langchain_community.vectorstores.chroma import Chroma\n",
    "from langchain_openai import ChatOpenAI\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "import os\n",
    "\n",
    "_ = load_dotenv(find_dotenv())    # read local .env file\n",
    "zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']\n",
    "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]\n",
    "\n",
    "# 定义 Embeddings\n",
    "embedding = ZhipuAIEmbeddings()\n",
    "\n",
    "# 向量数据库持久化路径\n",
    "persist_directory = '../../data_base/vector_db/chroma'\n",
    "\n",
    "# 加载数据库\n",
    "vectordb = Chroma(\n",
    "    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上\n",
    "    embedding_function=embedding\n",
    ")\n",
    "retriever = vectordb.as_retriever()\n",
    "# 使用 OpenAI GPT-4o 模型\n",
    "llm = ChatOpenAI(model_name = \"gpt-4o\", temperature = 0)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在系统开发的初期，验证集体量较小，最简单、直观的方法即为人工对验证集中的每一个验证案例进行评估。但是，人工评估也有一些基本准则与思路，此处简要介绍供学习者参考。但请注意，系统的评估与业务强相关，设计具体的评估方法与维度需要结合具体业务深入考虑。\n",
    "\n",
    "#### 5.1.2.1 量化评估\n",
    "\n",
    "为保证很好地比较不同版本的系统性能，量化评估指标是非常必要的。我们应该对每一个验证案例的回答都给出打分，最后计算所有验证案例的平均分得到本版本系统的得分。量化的量纲可以是0~5，也可以是0~100，可以根据个人风格和业务实际情况而定。\n",
    "\n",
    "量化后的评估指标应当有一定的评估规范，例如在满足条件 A 的情况下可以打分为 y 分，以保证不同评估员之间评估的相对一致。\n",
    "\n",
    "例如，我们给出两个验证案例：\n",
    "\n",
    "① 《南瓜书》的作者是谁？\n",
    "\n",
    "② 应该如何使用南瓜书？\n",
    "\n",
    "接下来我们分别用版本A prompt（简明扼要） 与版本B prompt（详细具体） 来要求模型做出回答："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "问题一：\n",
      "南瓜书是以西瓜书为前置知识进行表述的，旨在对西瓜书中较难理解的公式进行解析和补充推导细节。南瓜书的最佳使用方法是以西瓜书为主线，遇到推导不出来或看不懂的公式时再查阅南瓜书。谢谢你的提问！\n",
      "问题二：\n",
      "使用南瓜书的最佳方法是以西瓜书为主线，当遇到推导不出或看不懂的公式时，再查阅南瓜书。对于初学者，建议先简单浏览西瓜书的第1章和第2章的公式，等有一定基础后再深入研究。若南瓜书中没有你需要的内容，可以通过GitHub的Issues反馈或联系编委会。谢谢你的提问！\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_core.runnables import RunnablePassthrough, RunnableParallel, RunnableLambda\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "template_v1 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问！”。\n",
    "{context}\n",
    "问题: {question}\n",
    "\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(template=template_v1)\n",
    "\n",
    "def combine_docs(docs):\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
    "retrievel_chain = retriever | RunnableLambda(combine_docs)\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    |{\n",
    "        \"answer\": QA_CHAIN_PROMPT | llm | StrOutputParser(),\n",
    "        \"context\": lambda x: x[\"context\"]\n",
    "    }\n",
    ")\n",
    "print(\"问题一：\")\n",
    "question = \"南瓜书和西瓜书有什么关系？\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result[\"answer\"])\n",
    "\n",
    "print(\"问题二：\")\n",
    "question = \"应该如何使用南瓜书？\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result[\"answer\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上述是版本A Prompt 的回答，我们再测试版本B："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "问题一：\n",
      "南瓜书和西瓜书之间的关系是，南瓜书是以西瓜书为基础进行扩展和补充的学习资料。具体来说：\n",
      "\n",
      "1. **西瓜书**：由周志华老师编写，是机器学习领域的经典入门教材。书中对部分公式的推导细节没有详述，假设读者具有一定的数学基础，能够自行推导或通过练习掌握这些细节。\n",
      "\n",
      "2. **南瓜书**：旨在帮助那些在自学过程中对西瓜书中的公式推导细节感到困难的读者。南瓜书对西瓜书中比较难理解的公式进行解析，并补充具体的推导细节。它是以西瓜书的内容为前置知识进行表述的，建议读者在学习西瓜书时遇到困难再查阅南瓜书。\n",
      "\n",
      "因此，南瓜书可以被视为西瓜书的辅助学习工具，特别适合那些需要更详细推导过程的读者。\n",
      "问题二：\n",
      "使用南瓜书的最佳方法是将其作为西瓜书的补充材料，特别是在遇到推导不出来或者看不懂的公式时。以下是一些具体的使用建议：\n",
      "\n",
      "1. **结合西瓜书使用**：南瓜书的内容是基于西瓜书的前置知识进行表述的。因此，建议以西瓜书为主线进行学习，当遇到难以理解的公式时，再查阅南瓜书以获取更详细的推导和解释。\n",
      "\n",
      "2. **初学者建议**：对于刚开始学习机器学习的初学者，建议不要过于深究西瓜书第1章和第2章的公式。可以先简单了解，等到对机器学习有了一定的基础和理解后，再回过头来深入研究这些公式。\n",
      "\n",
      "3. **数学基础视角**：南瓜书力求以本科数学基础的视角进行公式的解析和推导。如果涉及到超出本科数学基础的知识，南瓜书通常会在附录和参考文献中提供相关资料，供有兴趣的读者进一步学习。\n",
      "\n",
      "4. **反馈和互动**：如果南瓜书中没有你需要的公式，或者发现了错误，可以通过GitHub的Issues页面进行反馈。编委会通常会在24小时内回复。此外，还可以通过微信联系编委会。\n",
      "\n",
      "5. **配套资源**：南瓜书还提供了配套的视频教程和在线阅读地址，可以通过这些资源更全面地理解书中的内容。\n",
      "\n",
      "6. **加入读者交流群**：通过扫描二维码并回复关键词“南瓜书”，可以加入读者交流群，与其他读者交流学习经验。\n",
      "\n",
      "通过以上方法，读者可以更有效地利用南瓜书来辅助学习机器学习中的复杂公式和概念。\n"
     ]
    }
   ],
   "source": [
    "template_v2 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate.from_template(template_v2)\n",
    "\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | {\n",
    "        \"answer\": QA_CHAIN_PROMPT | llm | StrOutputParser(),\n",
    "        \"context\": lambda x: x[\"context\"]\n",
    "    }\n",
    ")\n",
    "\n",
    "print(\"问题一：\")\n",
    "question = \"南瓜书和西瓜书有什么关系？\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result[\"answer\"])\n",
    "\n",
    "print(\"问题二：\")\n",
    "question = \"应该如何使用南瓜书？\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result[\"answer\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，版本 A 的 prompt 在案例①上有着更好的效果，但版本 B 的 prompt 在案例②上效果更佳。如果我们不量化评估指标，仅使用相对优劣的评估的话，我们无法判断版本 A 与版本 B 哪一个 prompt 更好，从而要找到一个 prompt 在所有案例上表现都更优才能进一步迭代；然而，这很明显是非常困难且不利于我们迭代优化的。\n",
    "\n",
    "我们可以给每个答案赋予 1~5 的打分。例如，在上述案例中，我们给版本 A 的答案①打分为4，答案②打分为2，给版本 B 的答案①打分为3，答案②打分为5；那么，版本 A 的平均得分为3分，版本 B 的平均得分为4分，则版本 B 优于版本 A。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1.2.2 多维评估\n",
    "\n",
    "大模型是典型的生成模型，即其回答为一个由模型生成的语句。一般而言，大模型的回答需要在多个维度上进行评估。例如，本项目的个人知识库问答项目上，用户提问一般是针对个人知识库的内容进行提问，模型的回答需要同时满足充分使用个人知识库内容、答案与问题一致、答案真实有效、回答语句通顺等。一个优秀的问答助手，应当既能够很好地回答用户的问题，保证答案的正确性，又能够体现出充分的智能性。\n",
    "\n",
    "因此，我们往往需要从多个维度出发，设计每个维度的评估指标，在每个维度上都进行打分，从而综合评估系统性能。同时需要注意的是，多维评估应当和量化评估有效结合，对每一个维度，可以设置相同的量纲也可以设置不同的量纲，应充分结合业务实际。\n",
    "\n",
    "例如，在本项目中，我们可以设计如下几个维度的评估：\n",
    "\n",
    "① 知识查找正确性。该维度需要查看系统从向量数据库查找相关知识片段的中间结果，评估系统查找到的知识片段是否能够对问题做出回答。该维度为0-1评估，即打分为0指查找到的知识片段不能做出回答，打分为1指查找到的知识片段可以做出回答。\n",
    "\n",
    "② 回答一致性。该维度评估系统的回答是否针对用户问题展开，是否有偏题、错误理解题意的情况，该维度量纲同样设计为0~1，0为完全偏题，1为完全切题，中间结果可以任取。\n",
    "\n",
    "③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段，评估系统的回答是否出现幻觉，幻觉比例有多高。该维度同样设计为0~1,0为全部是模型幻觉，1为没有任何幻觉。\n",
    "\n",
    "④ 回答正确性。该维度评估系统回答是否正确，是否充分解答了用户问题，是系统最核心的评估指标之一。该维度可以在0~1之间任意打分。\n",
    "\n",
    "上述四个维度都围绕知识、回答的正确性展开，与问题高度相关；接下来几个维度将围绕大模型生成结果的拟人性、语法正确性展开，与问题相关性较小：\n",
    "\n",
    "⑤ 逻辑性。该维度评估系统回答是否逻辑连贯，是否出现前后冲突、逻辑混乱的情况。该维度为0-1评估。\n",
    "\n",
    "⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法，可以在0~1之间任意打分。\n",
    "\n",
    "⑦ 智能性。该维度评估系统回答是否拟人化、智能化，是否能充分让用户混淆人工回答与智能回答。该维度可以在0~1之间任意打分。\n",
    "\n",
    "例如，我们针对以下回答进行评估："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "问题：\n",
      "应该如何使用南瓜书？\n",
      "模型回答：\n",
      "使用南瓜书的最佳方法是将其作为西瓜书的补充资料，特别是在遇到推导不出来或者看不懂的公式时，可以查阅南瓜书以获得更详细的解析和推导细节。以下是一些具体的使用建议：\n",
      "\n",
      "1. **结合西瓜书使用**：南瓜书的内容是基于西瓜书的前置知识进行表述的，因此建议以西瓜书为主线进行学习。当遇到困难时，再查阅南瓜书以获得更深入的理解。\n",
      "\n",
      "2. **初学者建议**：对于初学者，特别是刚接触机器学习的小白，建议不要过于深究西瓜书第1章和第2章的公式。可以先简单了解，等到对机器学习有更深入的理解后再回头研究这些公式。\n",
      "\n",
      "3. **数学基础视角**：南瓜书力求以本科数学基础的视角进行讲解，因此对于超出本科数学范围的知识，通常会以附录和参考文献的形式提供，感兴趣的读者可以根据这些资料进行深入学习。\n",
      "\n",
      "4. **反馈和互动**：如果南瓜书中没有你需要的公式，或者发现了错误，可以通过GitHub的Issues页面进行反馈。编委会通常会在24小时内回复。\n",
      "\n",
      "5. **配套资源**：可以利用南瓜书提供的配套视频教程和在线阅读地址进行学习。此外，还可以通过扫描二维码加入“南瓜书读者交流群”以获得更多交流和支持。\n",
      "\n",
      "6. **获取最新版本**：可以通过GitHub获取最新版的PDF，以确保学习到最新的内容和修订。\n",
      "\n",
      "通过这些方法，南瓜书可以帮助你更好地理解和掌握机器学习中的复杂公式和概念。\n"
     ]
    }
   ],
   "source": [
    "print(\"问题：\")\n",
    "question = \"应该如何使用南瓜书？\"\n",
    "print(question)\n",
    "print(\"模型回答：\")\n",
    "result = qa_chain.invoke(question)\n",
    "print(result[\"answer\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以下是系统查找到的知识片段："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "前言\n",
      "“周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读\n",
      "者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推\n",
      "导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充\n",
      "具体的推导细节。”\n",
      "读到这里，大家可能会疑问为啥前面这段话加了引号，因为这只是我们最初的遐想，后来我们了解到，周\n",
      "老师之所以省去这些推导细节的真实原因是，他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\n",
      "中的推导细节无困难吧，要点在书里都有了，略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\n",
      "等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\n",
      "下学生”。\n",
      "使用说明\n",
      "• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的，所以南瓜书的最佳使用方法是以西瓜书\n",
      "为主线，遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书；\n",
      "• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\n",
      "\n",
      "• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\n",
      "有点飘的时候再回来啃都来得及；\n",
      "• 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解，所以超纲的数学知识\n",
      "我们通常都会以附录和参考文献的形式给出，感兴趣的同学可以继续沿着我们给的资料进行深入学习；\n",
      "• 若南瓜书里没有你想要查阅的公式，或者你发现南瓜书哪个地方有错误，请毫不犹豫地去我们GitHub 的\n",
      "Issues（地址：https://github.com/datawhalechina/pumpkin-book/issues）进行反馈，在对应版块\n",
      "提交你希望补充的公式编号或者勘误信息，我们通常会在24 小时以内给您回复，超过24 小时未回复的\n",
      "话可以微信联系我们（微信号：at-Sm1les）；\n",
      "配套视频教程：https://www.bilibili.com/video/BV1Mh411e7VU\n",
      "在线阅读地址：https://datawhalechina.github.io/pumpkin-book（仅供第1 版）\n",
      "\n",
      "最新版PDF 获取地址：https://github.com/datawhalechina/pumpkin-book/releases\n",
      "编委会\n",
      "主编：Sm1les、archwalker、jbb0523\n",
      "编委：juxiao、Majingmin、MrBigFan、shanry、Ye980226\n",
      "封面设计：构思-Sm1les、创作-林王茂盛\n",
      "致谢\n",
      "特别感谢awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\n",
      "spareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。\n",
      "扫描下方二维码，然后回复关键词“南瓜书”，即可加入“南瓜书读者交流群”\n",
      "版权声明\n",
      "本作品采用知识共享署名-非商业性使用-相同方式共享4.0 国际许可协议进行许可。\n",
      "\n",
      "→_→\n",
      "欢迎去各大电商平台选购纸质版南瓜书《机器学习公式详解》\n",
      "←_←\n",
      "9.2.4\n",
      "式(9.8) 的解释. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
      "98\n",
      "9.2.5\n",
      "式(9.12) 的解释\n",
      ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
      "98\n",
      "9.3\n",
      "距离计算\n",
      ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
      "98\n",
      "9.3.1\n",
      "式(9.21) 的解释\n",
      ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
      "99\n",
      "9.4\n",
      "原型聚类\n"
     ]
    }
   ],
   "source": [
    "print(result[\"context\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们做出相应评估：\n",
    "\n",
    "① 知识查找正确性——1\n",
    "\n",
    "② 回答一致性——0.8（解答了问题，但是类似于“反馈”的话题偏题了）\n",
    "\n",
    "③ 回答幻觉比例——1\n",
    "\n",
    "④ 回答正确性——0.8（理由同上）\n",
    "\n",
    "⑤ 逻辑性——0.7（后续内容与前面逻辑连贯性不强）\n",
    "\n",
    "⑥ 通顺性——0.6（最后总结啰嗦且无效）\n",
    "\n",
    "⑦ 智能性——0.5（具有 AI 回答的显著风格）\n",
    "\n",
    "综合上述七个维度，我们可以全面、综合地评估系统在每个案例上的表现，综合考虑所有案例的得分，就可以评估系统在每个维度的表现。如果将所有维度量纲统一，那么我们还可以计算所有维度的平均得分来评估系统的得分。我们也可以针对不同维度的不同重要性赋予权值，再计算所有维度的加权平均来代表系统得分。\n",
    "\n",
    "但是，我们可以看到，越全面、具体的评估，其评估难度、评估成本就越大。以上述七维评估为例，对系统每一个版本的每一个案例，我们都需要进行七次评估。如果我们有两个版本的系统，验证集中有10个验证案例，那么我们每一次评估就需要 $ 10 \\times 2 \\times 7 = 140$ 次；但当我们的系统不断改进迭代，验证集会迅速扩大，一般来说，一个成熟的系统验证集应该至少在几百的体量，迭代改进版本至少有数十个，那么我们评估的总次数会达到上万次，带来的人力成本与时间成本就很高了。因此，我们需要一种自动评估模型回答的方法。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1.2.4 构造客观题\n",
    "\n",
    "大模型评估之所以复杂，一个重要原因在于生成模型的答案很难判别，即客观题评估判别很简单，主观题评估判别则很困难。尤其是对于一些没有标准答案的问题，实现自动评估就显得难度尤大。但是，在牺牲一定评估准确性的情况下，我们可以将复杂的没有标准答案的主观题进行转化，从而变成有标准答案的问题，进而通过简单的自动评估来实现。此处介绍两种方法：构造客观题与计算标准答案相似度。\n",
    "\n",
    "主观题的评估是非常困难的，但是客观题可以直接对比系统答案与标准答案是否一致，从而实现简单评估。我们可以将部分主观题构造为多项或单项选择的客观题，进而实现简单评估。例如，对于问题：\n",
    "\n",
    "    【问答题】南瓜书的作者是谁？\n",
    "\n",
    "我们可以将该主观题构造为如下客观题：\n",
    "\n",
    "    【多项选择题】南瓜书的作者是谁？   A 周志明 B 谢文睿 C 秦州 D 贾彬彬\n",
    "\n",
    "要求模型回答该客观题，我们给定标准答案为 BCD，将模型给出答案与标准答案对比即可实现评估打分。根据以上思想，我们可以构造出一个 Prompt 问题模板："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt_template = '''\n",
    "请你做如下选择题：\n",
    "题目：南瓜书的作者是谁？\n",
    "选项：A 周志明 B 谢文睿 C 秦州 D 贾彬彬\n",
    "你可以参考的知识片段：\n",
    "```\n",
    "{}\n",
    "```\n",
    "请仅返回选择的选项\n",
    "如果你无法做出选择，请返回空\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当然，由于大模型的不稳定性，即使我们要求其只给出选择选项，系统可能也会返回一大堆文字，其中详细解释了为什么选择如下选项。因此，我们需要将选项从模型回答中抽取出来。同时，我们需要设计一个打分策略。一般情况下，我们可以使用多选题的一般打分策略：全选1分，漏选0.5分，错选不选不得分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def multi_select_score_v1(true_answer : str, generate_answer : str) -> float:\n",
    "    # true_anser : 正确答案，str 类型，例如 'BCD'\n",
    "    # generate_answer : 模型生成答案，str 类型\n",
    "    true_answers = list(true_answer)\n",
    "    '''为便于计算，我们假设每道题都只有 A B C D 四个选项'''\n",
    "    # 先找出错误答案集合\n",
    "    false_answers = [item for item in ['A', 'B', 'C', 'D'] if item not in true_answers]\n",
    "    # 如果生成答案出现了错误答案\n",
    "    for one_answer in false_answers:\n",
    "        if one_answer in generate_answer:\n",
    "            return 0\n",
    "    # 再判断是否全选了正确答案\n",
    "    if_correct = 0\n",
    "    for one_answer in true_answers:\n",
    "        if one_answer in generate_answer:\n",
    "            if_correct += 1\n",
    "            continue\n",
    "    if if_correct == 0:\n",
    "        # 不选\n",
    "        return 0\n",
    "    elif if_correct == len(true_answers):\n",
    "        # 全选\n",
    "        return 1\n",
    "    else:\n",
    "        # 漏选\n",
    "        return 0.5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "基于上述打分函数，我们可以测试四个回答：\n",
    "\n",
    "① B C\n",
    "\n",
    "② 除了 A 周志华之外，其他都是南瓜书的作者\n",
    "\n",
    "③ 应该选择 B C D\n",
    "\n",
    "④ 我不知道"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "答案一得分： 0.5\n",
      "答案二得分： 0\n",
      "答案三得分： 1\n",
      "答案四得分： 0\n"
     ]
    }
   ],
   "source": [
    "answer1 = 'B C'\n",
    "answer2 = '西瓜书的作者是 A 周志华'\n",
    "answer3 = '应该选择 B C D'\n",
    "answer4 = '我不知道'\n",
    "true_answer = 'BCD'\n",
    "print(\"答案一得分：\", multi_select_score_v1(true_answer, answer1))\n",
    "print(\"答案二得分：\", multi_select_score_v1(true_answer, answer2))\n",
    "print(\"答案三得分：\", multi_select_score_v1(true_answer, answer3))\n",
    "print(\"答案四得分：\", multi_select_score_v1(true_answer, answer4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是我们可以看到，我们要求模型在不能回答的情况下不做选择，而不是随便选。但是在我们的打分策略中，错选和不选均为0分，这样其实鼓励了模型的幻觉回答，因此我们可以根据情况调整打分策略，让错选扣一分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def multi_select_score_v2(true_answer : str, generate_answer : str) -> float:\n",
    "    # true_anser : 正确答案，str 类型，例如 'BCD'\n",
    "    # generate_answer : 模型生成答案，str 类型\n",
    "    true_answers = list(true_answer)\n",
    "    '''为便于计算，我们假设每道题都只有 A B C D 四个选项'''\n",
    "    # 先找出错误答案集合\n",
    "    false_answers = [item for item in ['A', 'B', 'C', 'D'] if item not in true_answers]\n",
    "    # 如果生成答案出现了错误答案\n",
    "    for one_answer in false_answers:\n",
    "        if one_answer in generate_answer:\n",
    "            return -1\n",
    "    # 再判断是否全选了正确答案\n",
    "    if_correct = 0\n",
    "    for one_answer in true_answers:\n",
    "        if one_answer in generate_answer:\n",
    "            if_correct += 1\n",
    "            continue\n",
    "    if if_correct == 0:\n",
    "        # 不选\n",
    "        return 0\n",
    "    elif if_correct == len(true_answers):\n",
    "        # 全选\n",
    "        return 1\n",
    "    else:\n",
    "        # 漏选\n",
    "        return 0.5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如上，我们使用第二版本的打分函数再次对四个答案打分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "答案一得分： 0.5\n",
      "答案二得分： -1\n",
      "答案三得分： 1\n",
      "答案四得分： 0\n"
     ]
    }
   ],
   "source": [
    "answer1 = 'B C'\n",
    "answer2 = '西瓜书的作者是 A 周志华'\n",
    "answer3 = '应该选择 B C D'\n",
    "answer4 = '我不知道'\n",
    "true_answer = 'BCD'\n",
    "print(\"答案一得分：\", multi_select_score_v2(true_answer, answer1))\n",
    "print(\"答案二得分：\", multi_select_score_v2(true_answer, answer2))\n",
    "print(\"答案三得分：\", multi_select_score_v2(true_answer, answer3))\n",
    "print(\"答案四得分：\", multi_select_score_v2(true_answer, answer4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，这样我们就实现了快速、自动又有区分度的自动评估。在这样的方法下，我们只需对每一个验证案例进行构造，之后每一次验证、迭代都可以完全自动化进行，从而实现了高效的验证。\n",
    "\n",
    "但是，不是所有的案例都可以构造为客观题，针对一些不能构造为客观题或构造为客观题会导致题目难度骤降的情况，我们需要用到第二种方法：计算答案相似度。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1.2.5 计算答案相似度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "生成问题的答案评估在 NLP 中实则也不是一个新问题了，不管是机器翻译、自动文摘等任务，其实都需要评估生成答案的质量。NLP 一般对生成问题采用人工构造标准答案并计算回答与标准答案相似度的方法来实现自动评估。\n",
    "\n",
    "例如，对问题：\n",
    "\n",
    "    南瓜书的目标是什么？\n",
    "\n",
    "我们可以首先人工构造一个标准回答：\n",
    "\n",
    "    周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充具体的推导细节。\n",
    "\n",
    "接着对模型回答计算其与该标准回答的相似程度，越相似则我们认为答案正确程度越高。\n",
    "\n",
    "计算相似度的方法有很多，我们一般可以使用 BLEU 来计算相似度，其原理详见：[知乎|BLEU详解](https://zhuanlan.zhihu.com/p/223048748)，对于不想深究算法原理的同学，可以简单理解为主题相似度。\n",
    "\n",
    "我们可以调用 nltk 库中的 bleu 打分函数来计算："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.translate.bleu_score import sentence_bleu\n",
    "import jieba\n",
    "\n",
    "def bleu_score(true_answer : str, generate_answer : str) -> float:\n",
    "    # true_anser : 标准答案，str 类型\n",
    "    # generate_answer : 模型生成答案，str 类型\n",
    "    true_answers = list(jieba.cut(true_answer))\n",
    "    # print(true_answers)\n",
    "    generate_answers = list(jieba.cut(generate_answer))\n",
    "    # print(generate_answers)\n",
    "    bleu_score = sentence_bleu(true_answers, generate_answers)\n",
    "    return bleu_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "测试一下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Building prefix dict from the default dictionary ...\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "答案一：\n",
      "周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充具体的推导细节。\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Dumping model to file cache /var/folders/yd/c4q_f88j1l70g7_jcb6pdnb80000gn/T/jieba.cache\n",
      "Loading model cost 0.308 seconds.\n",
      "Prefix dict has been built successfully.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "得分： 1.2705543769116016e-231\n",
      "答案二：\n",
      "本南瓜书只能算是我等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二下学生”\n",
      "得分： 1.1935398790363042e-231\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/lta/anaconda3/envs/universe_0_3/lib/python3.10/site-packages/nltk/translate/bleu_score.py:577: UserWarning: \n",
      "The hypothesis contains 0 counts of 2-gram overlaps.\n",
      "Therefore the BLEU score evaluates to 0, independently of\n",
      "how many N-gram overlaps of lower order it contains.\n",
      "Consider using lower n-gram order or use SmoothingFunction()\n",
      "  warnings.warn(_msg)\n",
      "/Users/lta/anaconda3/envs/universe_0_3/lib/python3.10/site-packages/nltk/translate/bleu_score.py:577: UserWarning: \n",
      "The hypothesis contains 0 counts of 3-gram overlaps.\n",
      "Therefore the BLEU score evaluates to 0, independently of\n",
      "how many N-gram overlaps of lower order it contains.\n",
      "Consider using lower n-gram order or use SmoothingFunction()\n",
      "  warnings.warn(_msg)\n",
      "/Users/lta/anaconda3/envs/universe_0_3/lib/python3.10/site-packages/nltk/translate/bleu_score.py:577: UserWarning: \n",
      "The hypothesis contains 0 counts of 4-gram overlaps.\n",
      "Therefore the BLEU score evaluates to 0, independently of\n",
      "how many N-gram overlaps of lower order it contains.\n",
      "Consider using lower n-gram order or use SmoothingFunction()\n",
      "  warnings.warn(_msg)\n"
     ]
    }
   ],
   "source": [
    "true_answer = '周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充具体的推导细节。'\n",
    "\n",
    "print(\"答案一：\")\n",
    "answer1 = '周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充具体的推导细节。'\n",
    "print(answer1)\n",
    "score = bleu_score(true_answer, answer1)\n",
    "print(\"得分：\", score)\n",
    "print(\"答案二：\")\n",
    "answer2 = '本南瓜书只能算是我等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二下学生”'\n",
    "print(answer2)\n",
    "score = bleu_score(true_answer, answer2)\n",
    "print(\"得分：\", score)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，答案与标准答案一致性越高，则评估打分就越高。通过此种方法，我们同样只需对验证集中每一个问题构造一个标准答案，之后就可以实现自动、高效的评估。\n",
    "\n",
    "但是，该种方法同样存在几个问题：① 需要人工构造标准答案。对于一些垂直领域而言，构造标准答案可能是一件困难的事情；② 通过相似度来评估，可能存在问题。例如，如果生成回答与标准答案高度一致但在核心的几个地方恰恰相反导致答案完全错误，bleu 得分仍然会很高；③ 通过计算与标准答案一致性灵活性很差，如果模型生成了比标准答案更好的回答，但评估得分反而会降低；④ 无法评估回答的智能性、流畅性。如果回答是各个标准答案中的关键词拼接出来的，我们认为这样的回答是不可用无法理解的，但 bleu 得分会较高。\n",
    "\n",
    "因此，针对业务情况，有时我们还需要一些不需要构造标准答案的、进阶的评估方法。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1.2.6 使用大模型进行评估"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用人工评估准确度高、全面性强，但人力成本与时间成本高；使用自动评估成本低、评估速度快，但存在准确性不足、评估不够全面的问题。那么，我们是否有一种方法综合两者的优点，实现快速、全面的生成问题评估呢？\n",
    "\n",
    "以 GPT-4 为代表的大模型为我们提供了一种新的方法：使用大模型进行评估。我们可以通过构造 Prompt Engineering 让大模型充当一个评估者的角色，从而替代人工评估的评估员；同时大模型可以给出类似于人工评估的结果，因此可以采取人工评估中的多维度量化评估的方式，实现快速全面的评估。\n",
    "\n",
    "例如，我们可以构造如下的 Prompt Engineering，让大模型进行打分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt = '''\n",
    "你是一个模型回答评估员。\n",
    "接下来，我将给你一个问题、对应的知识片段以及模型根据知识片段对问题的回答。\n",
    "请你依次评估以下维度模型回答的表现，分别给出打分：\n",
    "\n",
    "① 知识查找正确性。评估系统给定的知识片段是否能够对问题做出回答。如果知识片段不能做出回答，打分为0；如果知识片段可以做出回答，打分为1。\n",
    "\n",
    "② 回答一致性。评估系统的回答是否针对用户问题展开，是否有偏题、错误理解题意的情况，打分分值在0~1之间，0为完全偏题，1为完全切题。\n",
    "\n",
    "③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段，评估系统的回答是否出现幻觉，打分分值在0~1之间,0为全部是模型幻觉，1为没有任何幻觉。\n",
    "\n",
    "④ 回答正确性。该维度评估系统回答是否正确，是否充分解答了用户问题，打分分值在0~1之间，0为完全不正确，1为完全正确。\n",
    "\n",
    "⑤ 逻辑性。该维度评估系统回答是否逻辑连贯，是否出现前后冲突、逻辑混乱的情况。打分分值在0~1之间，0为逻辑完全混乱，1为完全没有逻辑问题。\n",
    "\n",
    "⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法。打分分值在0~1之间，0为语句完全不通顺，1为语句完全通顺没有任何语法问题。\n",
    "\n",
    "⑦ 智能性。该维度评估系统回答是否拟人化、智能化，是否能充分让用户混淆人工回答与智能回答。打分分值在0~1之间，0为非常明显的模型回答，1为与人工回答高度一致。\n",
    "\n",
    "你应该是比较严苛的评估员，很少给出满分的高评估。\n",
    "用户问题：\n",
    "```\n",
    "{}\n",
    "```\n",
    "待评估的回答：\n",
    "```\n",
    "{}\n",
    "```\n",
    "给定的知识片段：\n",
    "```\n",
    "{}\n",
    "```\n",
    "你应该返回给我一个可直接解析的 Python 字典，字典的键是如上维度，值是每一个维度对应的评估打分。\n",
    "不要输出任何其他内容。\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以实际测试一下其效果："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'```python\\n{\\n    \"知识查找正确性\": 1,\\n    \"回答一致性\": 1,\\n    \"回答幻觉比例\": 0.9,\\n    \"回答正确性\": 0.9,\\n    \"逻辑性\": 1,\\n    \"通顺性\": 1,\\n    \"智能性\": 0.9\\n}\\n```'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 使用第二章讲过的 OpenAI 原生接口\n",
    "\n",
    "from openai import OpenAI\n",
    "\n",
    "client = OpenAI()\n",
    "\n",
    "\n",
    "def gen_gpt_messages(prompt):\n",
    "    '''\n",
    "    构造 GPT 模型请求参数 messages\n",
    "    \n",
    "    请求参数：\n",
    "        prompt: 对应的用户提示词\n",
    "    '''\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "    return messages\n",
    "\n",
    "\n",
    "def get_completion(prompt, model=\"gpt-4o\", temperature = 0):\n",
    "    '''\n",
    "    获取 GPT 模型调用结果\n",
    "\n",
    "    请求参数：\n",
    "        prompt: 对应的提示词\n",
    "        model: 调用的模型，默认为 gpt-4o，也可以按需选择 gpt-4 等其他模型\n",
    "        temperature: 模型输出的温度系数，控制输出的随机程度，取值范围是 0~2。温度系数越低，输出内容越一致。\n",
    "    '''\n",
    "    response = client.chat.completions.create(\n",
    "        model=model,\n",
    "        messages=gen_gpt_messages(prompt),\n",
    "        temperature=temperature,\n",
    "    )\n",
    "    if len(response.choices) > 0:\n",
    "        return response.choices[0].message.content\n",
    "    return \"generate answer error\"\n",
    "\n",
    "question = \"应该如何使用南瓜书？\"\n",
    "result = qa_chain.invoke(question)\n",
    "answer = result[\"answer\"]\n",
    "knowledge = result[\"context\"]\n",
    "\n",
    "response = get_completion(prompt.format(question, answer, knowledge))\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是注意，使用大模型进行评估仍然存在问题：\n",
    "\n",
    "① 我们的目标是迭代改进 Prompt 以提升大模型表现，因此我们所选用的评估大模型需要有优于我们所使用的大模型基座的性能，例如，目前性能最强大的大模型仍然是 GPT-4，推荐使用 GPT-4 来进行评估，效果最好。\n",
    "\n",
    "② 大模型具有强大的能力，但同样存在能力的边界。如果问题与回答太复杂、知识片段太长或是要求评估维度太多，即使是 GPT-4 也会出现错误评估、错误格式、无法理解指令等情况，针对这些情况，我们建议考虑如下方案来提升大模型表现：\n",
    "\n",
    "1. 改进 Prompt Engineering。以类似于系统本身 Prompt Engineering 改进的方式，迭代优化评估 Prompt Engineering，尤其是注意是否遵守了 Prompt Engineering 的基本准则、核心建议等；\n",
    "\n",
    "2. 拆分评估维度。如果评估维度太多，模型可能会出现错误格式导致返回无法解析，可以考虑将待评估的多个维度拆分，每个维度调用一次大模型进行评估，最后得到统一结果；\n",
    "\n",
    "3. 合并评估维度。如果评估维度太细，模型可能无法正确理解以至于评估不正确，可以考虑将待评估的多个维度合并，例如，将逻辑性、通顺性、智能性合并为智能性等；\n",
    "\n",
    "4. 提供详细的评估规范。如果没有评估规范，模型很难给出理想的评估结果。可以考虑给出详细、具体的评估规范，从而提升模型的评估能力；\n",
    "\n",
    "5. 提供少量示例。模型可能难以理解评估规范，此时可以给出少量评估的示例，供模型参考以实现正确评估。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1.2.7 混合评估"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "事实上，上述评估方法都不是孤立、对立的，相较于独立地使用某一种评估方法，我们更推荐将多种评估方法混合起来，对于每一种维度选取其适合的评估方法，兼顾评估的全面、准确和高效。\n",
    "\n",
    "例如，针对本项目个人知识库助手，我们可以设计以下混合评估方法：\n",
    "\n",
    "1. 客观正确性。客观正确性指对于一些有固定正确答案的问题，模型可以给出正确的回答。我们可以选取部分案例，使用构造客观题的方式来进行模型评估，评估其客观正确性。\n",
    "\n",
    "2. 主观正确性。主观正确性指对于没有固定正确答案的主观问题，模型可以给出正确的、全面的回答。我们可以选取部分案例，使用大模型评估的方式来评估模型回答是否正确。\n",
    "\n",
    "3. 智能性。智能性指模型的回答是否足够拟人化。由于智能性与问题本身弱相关，与模型、Prompt 强相关，且模型判断智能性能力较弱，我们可以少量抽样进行人工评估其智能性。\n",
    "\n",
    "4. 知识查找正确性。知识查找正确性指对于特定问题，从知识库检索到的知识片段是否正确、是否足够回答问题。知识查找正确性推荐使用大模型进行评估，即要求模型判别给定的知识片段是否足够回答问题。同时，该维度评估结果结合主观正确性可以计算幻觉情况，即如果主观回答正确但知识查找不正确，则说明产生了模型幻觉。\n",
    "\n",
    "使用上述评估方法，基于已得到的验证集示例，可以对项目做出合理评估。限于时间与人力，此处就不具体展示了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.2 评估并优化生成部分"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在前面的章节中，我们讲到了如何评估一个基于 RAG 框架的大模型应用的整体性能。通过针对性构造验证集，可以采用多种方法从多个维度对系统性能进行评估。但是，评估的目的是为了更好地优化应用效果，要优化应用性能，我们需要结合评估结果，对评估出的 Bad Case 进行拆分，并分别对每一部分做出评估和优化。\n",
    "\n",
    "RAG 全称为检索增强生成，因此，其有两个核心部分：检索部分和生成部分。检索部分的核心功能是保证系统根据用户 query 能够查找到对应的答案片段，而生成部分的核心功能即是保证系统在获得了正确的答案片段之后，可以充分发挥大模型能力生成一个满足用户要求的正确回答。\n",
    "\n",
    "优化一个大模型应用，我们往往需要从这两部分同时入手，分别评估检索部分和优化部分的性能，找出 Bad Case 并针对性进行性能的优化。而具体到生成部分，在已限定使用的大模型基座的情况下，我们往往会通过优化 Prompt Engineering 来优化生成的回答。在本章中，我们将首先结合我们刚刚搭建出的大模型应用实例——个人知识库助手，向大家讲解如何评估分析生成部分性能，针对性找出 Bad Case，并通过优化 Prompt Engineering 的方式来优化生成部分。\n",
    "\n",
    "在正式开始之前，我们先加载我们的向量数据库与检索链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/yd/c4q_f88j1l70g7_jcb6pdnb80000gn/T/ipykernel_26332/2254682865.py:23: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-chroma package and should be used instead. To use it run `pip install -U :class:`~langchain-chroma` and import as `from :class:`~langchain_chroma import Chroma``.\n",
      "  vectordb = Chroma(\n"
     ]
    }
   ],
   "source": [
    "import sys\n",
    "sys.path.append(\"../C3 搭建知识库\") # 将父目录放入系统路径中\n",
    "\n",
    "# 使用智谱 Embedding API，注意，需要将上一章实现的封装代码下载到本地\n",
    "from zhipuai_embedding import ZhipuAIEmbeddings\n",
    "\n",
    "from langchain_community.vectorstores.chroma import Chroma\n",
    "from langchain_openai import ChatOpenAI\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "import os\n",
    "\n",
    "_ = load_dotenv(find_dotenv())    # read local .env file\n",
    "zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']\n",
    "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]\n",
    "\n",
    "# 定义 Embeddings\n",
    "embedding = ZhipuAIEmbeddings()\n",
    "\n",
    "# 向量数据库持久化路径\n",
    "persist_directory = '../../data_base/vector_db/chroma'\n",
    "\n",
    "# 加载数据库\n",
    "vectordb = Chroma(\n",
    "    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上\n",
    "    embedding_function=embedding\n",
    ")\n",
    "\n",
    "# 使用 OpenAI GPT-4o 模型\n",
    "llm = ChatOpenAI(model_name = \"gpt-4o\", temperature = 0)\n",
    "\n",
    "# os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'\n",
    "# os.environ[\"HTTP_PROXY\"] = 'http://127.0.0.1:7890'\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们先使用初始化的 Prompt 创建一个基于模板的检索链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import PromptTemplate\n",
    "from langchain_core.runnables import RunnableParallel, RunnableLambda, RunnablePassthrough\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "template_v1 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问！”。\n",
    "{context}\n",
    "问题: {question}\n",
    "\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate.from_template(template_v1)\n",
    "\n",
    "def combine_docs(docs):\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
    "\n",
    "retrievel_chain = vectordb.as_retriever() | RunnableLambda(combine_docs)\n",
    "\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | QA_CHAIN_PROMPT\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "先测试一下效果："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "南瓜书是一本对《机器学习》（西瓜书）中较难理解的公式进行解析和推导细节补充的书籍，旨在帮助读者更好地理解机器学习中的数学公式。它以西瓜书为前置知识，适合在遇到推导困难时查阅。南瓜书的内容以本科数学基础为视角进行讲解，并提供配套视频教程和在线阅读资源。谢谢你的提问！\n"
     ]
    }
   ],
   "source": [
    "question = \"什么是南瓜书\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.1 提升直观回答质量"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "寻找 Bad Case 的思路有很多，最直观也最简单的就是评估直观回答的质量，结合原有资料内容，判断在什么方面有所不足。例如，上述的测试我们可以构造成一个 Bad Case：\n",
    "\n",
    "    问题：什么是南瓜书\n",
    "    初始回答：南瓜书是对《机器学习》（西瓜书）中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问！\n",
    "    存在不足：回答太简略，需要回答更具体；谢谢你的提问感觉比较死板，可以去掉\n",
    "我们再针对性地修改 Prompt 模板，加入要求其回答具体，并去掉“谢谢你的提问”的部分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "南瓜书是一本旨在帮助读者深入理解《机器学习》（西瓜书）中公式推导细节的辅助教材。它特别适合那些在学习西瓜书时遇到推导困难的读者。南瓜书的内容是基于西瓜书的前置知识进行表述的，因此建议读者在学习西瓜书时，遇到不理解的公式再查阅南瓜书。南瓜书的目标是帮助读者成为“理工科数学基础扎实点的大二下学生”，即具备扎实数学基础的学习者。\n",
      "\n",
      "南瓜书的编写者们力求以本科数学基础的视角进行公式的解析和推导，并提供附录和参考文献以供读者深入学习。南瓜书还提供了在线阅读、视频教程和读者交流群等资源，以便读者更好地学习和交流。\n",
      "\n",
      "如果读者在南瓜书中找不到需要的公式，或者发现错误，可以通过GitHub的Issues页面进行反馈，编委会通常会在24小时内回复。南瓜书的内容也在不断更新和完善，以更好地服务读者的学习需求。\n"
     ]
    }
   ],
   "source": [
    "template_v2 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate.from_template(template_v2)\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | QA_CHAIN_PROMPT\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "question = \"什么是南瓜书\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，改进后的 v2 版本能够给出更具体、详细的回答，解决了之前的问题。但是我们可以进一步思考，要求模型给出具体、详细的回答，是否会导致针对一些有要点的回答没有重点、模糊不清？我们测试以下问题："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "使用大语言模型（LLM）时，构造 Prompt 的原则主要包括以下两个关键点：\n",
      "\n",
      "1. **编写清晰、具体的指令**：\n",
      "   - 确保 Prompt 明确表达需求，提供足够的上下文信息，以便模型准确理解任务意图。就像向一个外星人解释人类世界一样，过于简略的 Prompt 可能导致模型无法把握任务的具体要求。\n",
      "   - 设计 Prompt 时，应避免模糊和不确定的语言，尽量使用具体的描述和明确的指令，以提高模型生成结果的准确性。\n",
      "\n",
      "2. **给予模型充足思考时间**：\n",
      "   - 让模型有足够的时间进行推理和思考，类似于人类在解题时需要时间思考以避免匆忙得出错误结论。\n",
      "   - 在 Prompt 中加入逐步推理的要求，确保模型有时间进行深度思考，从而生成更准确和可靠的结果。\n",
      "\n",
      "通过优化这两方面，开发者可以充分发挥语言模型的潜力，完成复杂的推理和生成任务。此外，Prompt 的设计是一个迭代优化的过程，开发者需要通过多次尝试和调整，逐步找到最适合应用的 Prompt 形式。\n"
     ]
    }
   ],
   "source": [
    "question = \"使用大模型时，构造 Prompt 的原则有哪些\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，针对我们关于 LLM 课程的提问，模型回答确实详细具体，也充分参考了课程内容，但回答使用首先、其次等词开头，同时将整体答案分成了4段，导致答案不是特别重点清晰，不容易阅读。因此，我们构造以下 Bad Case：\n",
    "\n",
    "    问题：使用大模型时，构造 Prompt 的原则有哪些\n",
    "    初始回答：略\n",
    "    存在不足：没有重点，模糊不清\n",
    "\n",
    "针对该 Bad Case，我们可以改进 Prompt，要求其对有几点的答案进行分点标号，让答案清晰具体：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "使用大模型时，构造 Prompt 的原则主要包括以下几点：\n",
      "\n",
      "1. **编写清晰、具体的指令**：\n",
      "   - 确保 Prompt 明确表达需求，提供足够的上下文信息，使语言模型能够准确理解任务意图。\n",
      "   - 避免过于简略的描述，以免模型难以把握具体任务要求。\n",
      "\n",
      "2. **给予模型充足思考时间**：\n",
      "   - 设计 Prompt 时，应加入逐步推理的要求，类似于人类解题过程，避免匆忙得出结论。\n",
      "   - 通过让模型有足够的时间进行推理，生成的结果会更加准确和可靠。\n",
      "\n",
      "3. **迭代优化**：\n",
      "   - 通过多次迭代优化 Prompt，逐步改进以达到最佳效果。\n",
      "   - 初版 Prompt 运行后，检查结果并分析不理想的原因，进行调整和改进。\n",
      "   - 对于复杂应用，可以在多个样本上进行迭代训练，评估 Prompt 的平均表现。\n",
      "\n",
      "4. **实践与评估**：\n",
      "   - 在 Jupyter Notebook 等工具上实践 Prompt 示例，观察不同输出，深入理解迭代优化过程。\n",
      "   - 在应用较为成熟后，采用在多个样本集上评估 Prompt 性能的方式进行细致优化。\n",
      "\n",
      "通过掌握这些原则，开发者可以更有效地利用大语言模型，构建出可靠的应用程序。\n"
     ]
    }
   ],
   "source": [
    "template_v3 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v3)\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | QA_CHAIN_PROMPT\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "question = \"使用大模型时，构造 Prompt 的原则有哪些\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "提升回答质量的方法还有很多，核心是围绕具体业务展开思考，找出初始回答中不足以让人满意的点，并针对性进行提升改进，此处不再赘述。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.2 标明知识来源，提高可信度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于大模型存在幻觉问题，有时我们会怀疑模型回答并非源于已有知识库内容，这对一些需要保证真实性的场景来说尤为重要，例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "我不知道强化学习的定义，因为提供的上下文中没有涉及到强化学习的相关内容。\n"
     ]
    }
   ],
   "source": [
    "question = \"强化学习的定义是什么\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以要求模型在生成回答时注明知识来源，这样可以避免模型杜撰并不存在于给定资料的知识，同时，也可以提高我们对模型生成答案的可信度："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "根据提供的上下文，强化学习的定义并未被直接提及，因此我无法从中得出强化学习的定义。不过，我可以提供一个通用的强化学习定义：\n",
      "\n",
      "强化学习（Reinforcement Learning, RL）是一种机器学习方法，旨在通过与环境的交互来学习如何采取行动，以最大化累积的奖励。强化学习的核心在于智能体（agent）在不同状态下选择动作（action），并根据环境反馈的奖励（reward）来调整策略（policy），以便在长期内获得最大的总奖励。强化学习通常涉及以下几个关键元素：\n",
      "\n",
      "1. **智能体（Agent）**：在环境中执行动作的实体。\n",
      "2. **环境（Environment）**：智能体与之交互的外部系统。\n",
      "3. **状态（State）**：环境在某一时刻的具体情况。\n",
      "4. **动作（Action）**：智能体在某一状态下可以执行的操作。\n",
      "5. **奖励（Reward）**：智能体执行动作后环境反馈的信号，用于指导智能体的学习。\n",
      "\n",
      "如果需要更详细的信息或具体的例子，建议查阅相关的强化学习教材或文献。\n"
     ]
    }
   ],
   "source": [
    "template_v4 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体。\n",
    "请你附上回答的来源原文，以保证回答的正确性。\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v4)\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | QA_CHAIN_PROMPT\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "question = \"强化学习的定义是什么\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是，附上原文来源往往会导致上下文的增加以及回复速度的降低，我们需要根据业务场景酌情考虑是否要求附上原文。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.3 构造思维链"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "大模型往往可以很好地理解并执行指令，但模型本身还存在一些能力的限制，例如大模型的幻觉、无法理解较为复杂的指令、无法执行复杂步骤等。我们可以通过构造思维链，将 Prompt 构造成一系列步骤来尽量减少其能力限制，例如，我们可以构造一个两步的思维链，要求模型在第二步做出反思，以尽可能消除大模型的幻觉问题。\n",
    "\n",
    "我们首先有这样一个 Bad Case：\n",
    "\n",
    "    问题：我们应该如何去构造一个 LLM 项目\n",
    "    初始回答：略\n",
    "    存在不足：事实上，知识库中中关于如何构造LLM项目的内容是使用 LLM API 去搭建一个应用，模型的回答看似有道理，实则是大模型的幻觉，将部分相关的文本拼接得到，存在问题"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "构造一个大型语言模型（LLM）项目是一个复杂的任务，需要考虑多个方面。以下是一些关键步骤和考虑因素：\n",
      "\n",
      "1. **项目目标和需求分析**：\n",
      "   - 明确项目的目标和应用场景，例如是用于对话系统、文本生成、翻译还是其他任务。\n",
      "   - 确定模型的性能指标，如准确性、响应速度和资源消耗等。\n",
      "\n",
      "2. **数据收集和准备**：\n",
      "   - 收集大量高质量的文本数据，确保数据的多样性和代表性。\n",
      "   - 对数据进行清洗和预处理，包括去除噪声、处理缺失值和标准化等。\n",
      "\n",
      "3. **模型选择和设计**：\n",
      "   - 选择合适的模型架构，如Transformer、GPT、BERT等，基于项目需求和资源限制。\n",
      "   - 考虑模型的规模和复杂度，平衡性能和计算资源。\n",
      "\n",
      "4. **训练和优化**：\n",
      "   - 配置训练环境，包括硬件（如GPU、TPU）和软件框架（如TensorFlow、PyTorch）。\n",
      "   - 进行模型训练，调整超参数以优化模型性能。\n",
      "   - 使用技术如迁移学习、微调等来提高模型的效率和效果。\n",
      "\n",
      "5. **评估和测试**：\n",
      "   - 使用标准数据集和指标对模型进行评估，确保其在不同任务上的表现。\n",
      "   - 进行A/B测试和用户测试，收集反馈以进一步改进模型。\n",
      "\n",
      "6. **部署和维护**：\n",
      "   - 将模型部署到生产环境中，确保其稳定性和可扩展性。\n",
      "   - 定期更新和维护模型，处理新数据和反馈，改进模型性能。\n",
      "\n",
      "7. **伦理和合规性**：\n",
      "   - 确保模型的使用符合伦理标准和法律法规，特别是在数据隐私和安全方面。\n",
      "   - 考虑模型的公平性和透明性，避免偏见和歧视。\n",
      "\n",
      "这些步骤需要跨学科的合作，包括数据科学、机器学习、软件工程和产品管理等领域的专业知识。通过系统化的方法和持续的迭代改进，可以成功构建和部署一个有效的LLM项目。\n"
     ]
    }
   ],
   "source": [
    "question = \"我们应该如何去构造一个LLM项目\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对此，我们可以优化 Prompt，将之前的 Prompt 变成两个步骤，要求模型在第二个步骤中做出反思："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "根据提供的上下文，以下是关于如何构造一个LLM（大语言模型）项目的详细步骤：\n",
      "\n",
      "1. **理解目标和需求**：\n",
      "   - 首先明确项目的目标和需求。了解你希望通过LLM实现什么样的功能或解决什么问题。\n",
      "   - 确定项目的范围和限制条件，比如预算、时间、技术栈等。\n",
      "\n",
      "2. **选择合适的模型和算法**：\n",
      "   - 根据项目需求选择合适的模型架构。可以考虑使用现有的预训练模型（如GPT、BERT等）进行微调，或者从头开始训练一个新的模型。\n",
      "   - 了解模型的基本原理和适用场景，确保选择的模型能够满足项目需求。\n",
      "\n",
      "3. **数据准备**：\n",
      "   - 收集和准备训练数据。数据的质量和数量对模型的性能至关重要。\n",
      "   - 对数据进行清洗和预处理，确保数据的一致性和准确性。\n",
      "\n",
      "4. **模型训练和优化**：\n",
      "   - 使用合适的框架（如TensorFlow、PyTorch等）进行模型训练。\n",
      "   - 通过调整超参数、优化算法等手段提高模型的性能。\n",
      "   - 使用交叉验证等方法评估模型的泛化能力。\n",
      "\n",
      "5. **评估和测试**：\n",
      "   - 对模型进行全面的评估和测试，确保其在不同场景下的表现。\n",
      "   - 使用合适的指标（如准确率、召回率、F1分数等）评估模型的性能。\n",
      "\n",
      "6. **部署和维护**：\n",
      "   - 将模型部署到生产环境中，确保其能够稳定运行。\n",
      "   - 定期对模型进行监控和维护，及时更新和优化模型以应对新的需求和挑战。\n",
      "\n",
      "7. **文档和用户培训**：\n",
      "   - 编写详细的项目文档，记录项目的各个方面，包括设计、实现、测试等。\n",
      "   - 为用户提供培训和支持，帮助他们更好地使用和理解模型。\n",
      "\n",
      "在以上步骤中，确保每一步都基于项目的具体需求和上下文进行调整和优化。\n",
      "\n",
      "反思：在提供的上下文中，并没有直接涉及如何构造一个LLM项目的具体步骤，因此以上回答是基于一般的项目构建流程和LLM的相关知识进行的总结。如果有任何不符合上下文的内容，请指出。\n"
     ]
    }
   ],
   "source": [
    "template_v4 = \"\"\"\n",
    "请你依次执行以下步骤：\n",
    "① 使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答案。\n",
    "你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体。\n",
    "上下文：\n",
    "{context}\n",
    "问题: \n",
    "{question}\n",
    "有用的回答:\n",
    "② 基于提供的上下文，反思回答中有没有不正确或不是基于上下文得到的内容，如果有，回答你不知道\n",
    "确保你执行了每一个步骤，不要跳过任意一个步骤。\n",
    "\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v4)\n",
    "qa_chain = (\n",
    "    RunnableParallel(context=retrievel_chain, question=RunnablePassthrough())\n",
    "    | QA_CHAIN_PROMPT\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "question = \"我们应该如何去构造一个LLM项目\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看出，要求模型做出自我反思之后，模型修复了自己的幻觉，给出了正确的答案。我们还可以通过构造思维链完成更多功能，此处就不再赘述了，欢迎读者尝试。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.4 增加一个指令解析"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们往往会面临一个需求，即我们需要模型以我们指定的格式进行输出。但是，由于我们使用了 Prompt Template 来填充用户问题，用户问题中存在的格式要求往往会被忽略，例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "① 基于提供的上下文，回答问题：\n",
      "\n",
      "LLM的分类是什么？给我返回一个 Python List\n",
      "\n",
      "有用的回答：\n",
      "\n",
      "1. 情感推断：LLM可以用于情感倾向分析，通过编写Prompt来解析评论的情感倾向，例如正面或负面。\n",
      "\n",
      "2. 识别语种：LLM可以识别文本的语言，例如识别“Combien coûte le lampadaire?”是法语。\n",
      "\n",
      "3. 多语种翻译：LLM可以将文本翻译成多种语言，例如将“I want to order a basketball.”翻译成中文、英文、法语和西班牙语。\n",
      "\n",
      "4. 同时进行语气转换：LLM可以根据输入的Prompt进行语气转换和信息提取，例如从文本中提取关于“美国航空航天局”的信息。\n",
      "\n",
      "Python List:\n",
      "```python\n",
      "llm_classifications = [\n",
      "    \"情感推断\",\n",
      "    \"识别语种\",\n",
      "    \"多语种翻译\",\n",
      "    \"同时进行语气转换\"\n",
      "]\n",
      "```\n",
      "\n",
      "② 反思回答中有没有不正确或不是基于上下文得到的内容：\n",
      "\n",
      "在提供的上下文中，LLM的分类是基于情感推断、识别语种、多语种翻译和语气转换等功能的描述得出的。因此，回答是基于上下文的，没有不正确的内容。\n"
     ]
    }
   ],
   "source": [
    "question = \"LLM的分类是什么？给我返回一个 Python List\"\n",
    "result = qa_chain.invoke(question)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，虽然我们要求模型给返回一个 Python List，但该输出要求被包裹在 Template 中被模型忽略掉了。针对该问题，我们可以构造一个 Bad Case：\n",
    "\n",
    "    问题：LLM的分类是什么？给我返回一个 Python List\n",
    "    初始回答：根据提供的上下文，LLM的分类可以分为基础LLM和指令微调LLM。\n",
    "    存在不足：没有按照指令中的要求输出\n",
    "\n",
    "针对该问题，一个存在的解决方案是，在我们的检索 LLM 之前，增加一层 LLM 来实现指令的解析，将用户问题的格式要求和问题内容拆分开来。这样的思路其实就是目前大火的 Agent 机制的雏形，即针对用户指令，设置一个 LLM（即 Agent）来理解指令，判断指令需要执行什么工具，再针对性调用需要执行的工具，其中每一个工具可以是基于不同 Prompt Engineering 的 LLM，也可以是例如数据库、API 等。LangChain 中其实有设计 Agent 机制，但本教程中我们就不再赘述了，这里只基于 OpenAI 的原生接口简单实现这一功能："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用第二章讲过的 OpenAI 原生接口\n",
    "\n",
    "from openai import OpenAI\n",
    "\n",
    "client = OpenAI()\n",
    "\n",
    "\n",
    "def gen_gpt_messages(prompt):\n",
    "    '''\n",
    "    构造 GPT 模型请求参数 messages\n",
    "    \n",
    "    请求参数：\n",
    "        prompt: 对应的用户提示词\n",
    "    '''\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "    return messages\n",
    "\n",
    "\n",
    "def get_completion(prompt, model=\"gpt-4o\", temperature = 0):\n",
    "    '''\n",
    "    获取 GPT 模型调用结果\n",
    "\n",
    "    请求参数：\n",
    "        prompt: 对应的提示词\n",
    "        model: 调用的模型，默认为 gpt-4o，也可以按需选择 gpt-4 等其他模型\n",
    "        temperature: 模型输出的温度系数，控制输出的随机程度，取值范围是 0~2。温度系数越低，输出内容越一致。\n",
    "    '''\n",
    "    response = client.chat.completions.create(\n",
    "        model=model,\n",
    "        messages=gen_gpt_messages(prompt),\n",
    "        temperature=temperature,\n",
    "    )\n",
    "    if len(response.choices) > 0:\n",
    "        return response.choices[0].message.content\n",
    "    return \"generate answer error\"\n",
    "\n",
    "prompt_input = '''\n",
    "请判断以下问题中是否包含对输出的格式要求，并按以下要求输出：\n",
    "请返回给我一个可解析的Python列表，列表第一个元素是对输出的格式要求，应该是一个指令；第二个元素是去掉格式要求的问题原文\n",
    "如果没有格式要求，请将第一个元素置为空\n",
    "需要判断的问题：\n",
    "```\n",
    "{}\n",
    "```\n",
    "不要输出任何其他内容或格式，确保返回结果可解析。\n",
    "'''\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们测试一下该 LLM 分解格式要求的能力：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'[\"给我返回一个 Python List\", \"LLM的分类是什么？\"]'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "response = get_completion(prompt_input.format(question))\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，通过上述 Prompt，LLM 可以很好地实现输出格式的解析，接下来，我们可以再设置一个 LLM 根据输出格式要求，对输出内容进行解析："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt_output = '''\n",
    "请根据回答文本和输出格式要求，按照给定的格式要求对问题做出回答\n",
    "需要回答的问题：\n",
    "```\n",
    "{}\n",
    "```\n",
    "回答文本：\n",
    "```\n",
    "{}\n",
    "```\n",
    "输出格式要求：\n",
    "```\n",
    "{}\n",
    "```\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后我们可以将两个 LLM 与检索链串联起来：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'```python\\n[\\n    \"生成式模型\",\\n    \"判别式模型\"\\n]\\n```'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "question = 'LLM的分类是什么？给我返回一个 Python List'\n",
    "# 首先将格式要求与问题拆分\n",
    "input_lst_s = get_completion(prompt_input.format(question))\n",
    "# 找到拆分之后列表的起始和结束字符\n",
    "start_loc = input_lst_s.find('[')\n",
    "end_loc = input_lst_s.find(']')\n",
    "rule, new_question = eval(input_lst_s[start_loc:end_loc+1])\n",
    "# 接着使用拆分后的问题调用检索链\n",
    "result = qa_chain.invoke(new_question)\n",
    "result_context = result\n",
    "# 接着调用输出格式解析\n",
    "response = get_completion(prompt_output.format(new_question, result_context, rule))\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，经过如上步骤，我们就成功地实现了输出格式的限定。当然，在上面代码中，核心为介绍 Agent 思想，事实上，不管是 Agent 机制还是 Parser 机制（也就是限定输出格式），LangChain 都提供了成熟的工具链供使用，欢迎感兴趣的读者深入探讨，此处就不展开讲解了。\n",
    "\n",
    "通过上述讲解的思路，结合实际业务情况，我们可以不断发现 Bad Case 并针对性优化 Prompt，从而提升生成部分的性能。但是，上述优化的前提是检索部分能够检索到正确的答案片段，也就是检索的准确率和召回率尽可能高。那么，如何能够评估并优化检索部分的性能呢？下一章我们会深入探讨这个问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 5.3 评估并优化检索部分\n",
    "\n",
    "\n",
    "在上一章，我们讲解了如何针对生成部分评估优化 Prompt Engineering，来提高大模型的生成质量。但生成的前提是检索，只有当我们应用的检索部分能够根据用户 query 检索到正确的答案文档时，大模型的生成结果才可能是正确的。因此，检索部分的检索精确率和召回率其实更大程度影响了应用的整体性能。但是，检索部分的优化是一个更工程也更深入的命题，我们往往需要使用到很多高级的、源于搜索的进阶技巧并探索更多实用工具，甚至手写一些工具来进行优化。因此，在本章中，我们仅大致讨论检索部分评估与优化的思路，而不深入展开代码实践。如果读者在阅读完本部分后感到意犹未尽，想要学习更多的进阶技巧来进一步优化自己的应用，欢迎阅读我们即将推出的教程第二部分《LLM 开发技巧》。\n",
    "\n",
    "### 5.3.1 评估检索效果\n",
    "\n",
    "首先让我们来回顾整个 RAG 系统的功能。\n",
    "\n",
    "![](../../assets/C5-3-rag.png)\n",
    "\n",
    "针对用户输入的一个 query，系统会将其转化为向量并在向量数据库中匹配最相关的文本段，然后根据我们的设定选择 3～5 个文本段落和用户的 query 一起交给大模型，再由大模型根据检索到的文本段落回答用户 query 中提出的问题。在这一整个系统中，我们将向量数据库检索相关文本段落的部分称为检索部分，将大模型根据检索到的文本段落进行答案生成的部分称为生成部分。\n",
    "\n",
    "因此，检索部分的核心功能是找到存在于知识库中、能够正确回答用户 query 中的提问的文本段落。因此，我们可以定义一个最直观的准确率在评估检索效果：对于 N 个给定 query，我们保证每一个 query 对应的正确答案都存在于知识库中。假设对于每一个 query，系统找到了 K 个文本片段，如果正确答案在 K 个文本片段之一，那么我们认为检索成功；如果正确答案不在 K 个文本片段之一，我们任务检索失败。那么，系统的检索准确率可以被简单地计算为：\n",
    "\n",
    "$$accuracy = \\frac{M}{N}$$\n",
    "\n",
    "其中，M 是成功检索的 query 数。\n",
    "\n",
    "通过上述准确率，我们可以衡量系统的检索能力，对于系统能成功检索到的 query，我们才能进一步优化 Prompt 来提高系统性能。对于系统检索失败的 query，我们就必须改进检索系统来优化检索效果。但是注意，当我们在计算如上定义的准确率时，一定要保证我们的每一个验证 query 的正确答案都确实存在于知识库中；如果正确答案本就不存在，那我们应该将 Bad Case 归因到知识库构建部分，说明知识库构建的广度和处理精度还有待提升。\n",
    "\n",
    "当然，这只是最简单的一种评估方式，事实上，这种评估方式存在很多不足。例如：\n",
    "\n",
    "- 有的 query 可能需要联合多个知识片段才能做出回答，对于这种 query，我们如何评估？\n",
    "- 检索到的知识片段彼此之间的顺序其实会对大模型的生成带来影响，我们是否应该将检索片段的排序纳入考虑？\n",
    "- 除去检索到正确的知识片段之外，我们的系统还应尽量避免检索到错误的、误导性知识片段，否则大模型的生成结果很可能被错误片段误导。我们是否应当将检索到的错误片段纳入指标计算？\n",
    "\n",
    "上述问题都不存在标准答案，需要针对项目实际针对的业务、评估的成本来综合考虑。\n",
    "\n",
    "除去通过上述方法来评估检索效果外，我们还可以将检索部分建模为一个经典的搜索任务。让我们来看看经典的搜索场景。搜索场景的任务是，针对用户给定的检索 query，从给定范围的内容（一般是网页）中找到相关的内容并进行排序，尽量使排序靠前的内容能够满足用户需求。\n",
    "\n",
    "其实我们的检索部分的任务和搜索场景非常类似，同样是针对用户 query，只不过我们相对更强调召回而非排序，以及我们检索的内容不是网页而是知识片段。因此，我们可以类似地将我们的检索任务建模为一个搜索任务，那么，我们就可以引入搜索算法中经典的评估思路（如准确率、召回率等）和优化思路（例如构建索引、重排等）来更充分地评估优化我们的检索效果。这部分就不再赘述，欢迎有兴趣的读者进行深入研究和分享。\n",
    "\n",
    "### 5.3.2 优化检索的思路\n",
    "\n",
    "上文陈述来评估检索效果的几种一般思路，当我们对系统的检索效果做出合理评估，找到对应的 Bad Case 之后，我们就可以将 Bad Case 拆解到多个维度来针对性优化检索部分。注意，虽然在上文评估部分，我们强调了评估检索效果的验证 query 一定要保证其正确答案存在于知识库之中，但是在此处，我们默认知识库构建也作为检索部分的一部分，因此，我们也需要在这一部分解决由于知识库构建有误带来的 Bad Case。在此，我们分享一些常见的 Bad Case 归因和可行的优化思路。\n",
    "\n",
    "**知识片段被割裂导致答案丢失**\n",
    "\n",
    "该问题一般表现为，对于一个用户 query，我们可以确定其问题一定是存在于知识库之中的，但是我们发现检索到的知识片段将正确答案分割开了，导致不能形成一个完整、合理的答案。该种问题在需要较长回答的 query 上较为常见。\n",
    "\n",
    "该类问题的一般优化思路是，优化文本切割方式。我们在《C3 搭建知识库》中使用到的是最原始的分割方式，即根据特定字符和 chunk 大小进行分割，但该类分割方式往往不能照顾到文本语义，容易造成同一主题的强相关上下文被切分到两个 chunk 总。对于一些格式统一、组织清晰的知识文档，我们可以针对性构建更合适的分割规则；对于格式混乱、无法形成统一的分割规则的文档，我们可以考虑纳入一定的人力进行分割。我们也可以考虑训练一个专用于文本分割的模型，来实现根据语义和主题的 chunk 切分。\n",
    "\n",
    "**query 提问需要长上下文概括回答**\n",
    "\n",
    "该问题也是存在于知识库构建的一个问题。即部分 query 提出的问题需要检索部分跨越很长的上下文来做出概括性回答，也就是需要跨越多个 chunk 来综合回答问题。但是由于模型上下文限制，我们往往很难给出足够的 chunk 数。\n",
    "\n",
    "该类问题的一般优化思路是，优化知识库构建方式。针对可能需要此类回答的文档，我们可以增加一个步骤，通过使用 LLM 来对长文档进行概括总结，或者预设提问让 LLM 做出回答，从而将此类问题的可能答案预先填入知识库作为单独的 chunk，来一定程度解决该问题。\n",
    "\n",
    "**关键词误导**\n",
    "\n",
    "该问题一般表现为，对于一个用户 query，系统检索到的知识片段有很多与 query 强相关的关键词，但知识片段本身并非针对 query 做出的回答。这种情况一般源于 query 中有多个关键词，其中次要关键词的匹配效果影响了主要关键词。\n",
    "\n",
    "该类问题的一般优化思路是，对用户 query 进行改写，这也是目前很多大模型应用的常用思路。即对于用户输入 query，我们首先通过 LLM 来将用户 query 改写成一种合理的形式，去除次要关键词以及可能出现的错字、漏字的影响。具体改写成什么形式根据具体业务而定，可以要求 LLM 对 query 进行提炼形成 Json 对象，也可以要求 LLM 对 query 进行扩写等。\n",
    "\n",
    "**匹配关系不合理**\n",
    "\n",
    "该问题是较为常见的，即匹配到的强相关文本段并没有包含答案文本。该问题的核心问题在于，我们使用的向量模型和我们一开始的假设不符。在讲解 RAG 的框架时，我们有提到，RAG 起效果是有一个核心假设的，即我们假设我们匹配到的强相关文本段就是问题对应的答案文本段。但是很多向量模型其实构建的是“配对”的语义相似度而非“因果”的语义相似度，例如对于 query-“今天天气怎么样”，会认为“我想知道今天天气”的相关性比“天气不错”更高。\n",
    "\n",
    "该类问题的一般优化思路是，优化向量模型或是构建倒排索引。我们可以选择效果更好的向量模型，或是收集部分数据，在自己的业务上微调一个更符合自己业务的向量模型。我们也可以考虑构建倒排索引，即针对知识库的每一个知识片段，构建一个能够表征该片段内容但和 query 的相对相关性更准确的索引，在检索时匹配索引和 query 的相关性而不是全文，从而提高匹配关系的准确性。\n",
    "\n",
    "优化检索部分的思路还有很多，事实上，检索部分的优化往往是 RAG 应用开发的核心工程部分。限于篇幅原因，此处就不再赘述更多的技巧及方法，欢迎有兴趣的读者阅读我们即将推出的第二部分《LLM 开发技巧》。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
